"""
Processing of user input to Web applications.

Because the Web is not a trusted environment, all input is considered tainted.
Further, as HTTP is a simple protocol, only one type of data may be given as
input: strings.  It is the job of this module to enable applications to assert
that input is well-formed and valid, and to translate it to native Python
values.
"""

__all__ = ['FormosaException', 'TranslationError', 'ValidationError',
           'ErrorSet', 'Form']

class FormosaException(Exception):
    """Base class for Formosa exceptions."""
    pass


class TranslationError(FormosaException):
    """Exception for when translation of a field fails."""
    pass


class ValidationError(FormosaException):
    """Exception raised when a higher-level validation constraint fails."""

    def __init__(self, message, keys):
        """Create a new exception for failed validation, where ``message``
        describes the reason for failure, and ``keys`` is an iterable of the
        offending keys in the subject."""
        self.message = message
        self.keys = keys

    def __str__(self):
        return self.message


class ErrorSet(FormosaException):
    """Exception for failed translation of user input.

    Instances serve as containers mapping errors to the fields to which they
    apply.  An error may be either a simple error message string, or it may be
    another ``ErrorSet`` instance in the case of nested errors.

    Each error (be it a message or an ``ErrorSet`` object) is associated with
    an iterable of zero or more field names, which is called the *target*.
    While the same error may be mapped to more than target, an error may be
    mapped to the same target only once.  The order in which field names are
    given is insignificant."""

    def __init__(self):
        super(ErrorSet, self).__init__()
        self._errors = {}

    def add(self, error, target=None):
        """Associate an error with the given iterable of target field names.
        Ignore the order of the field names."""
        if error not in self._errors:
            self._errors[error] = set()
        self._errors[error].add(frozenset(target or ()))

    def __len__(self):
        """Return the number of times each error maps to a different
        target."""
        return sum(len(i) for i in self._errors.itervalues())

    def targets(self):
        """Return the set of targets to which errors have been assigned."""
        targets = set()
        for i in self._errors.itervalues():
            for j in i:
                targets.add(j)
        return targets

    def iter_targets(self):
        """Yield each target to which an error has been assigned."""
        for i in self._errors.itervalues():
            for j in i:
                yield j

    def errors(self):
        """Return all errors that have been added to the collection."""
        return set(self._errors.keys())

    def iter_errors(self):
        """Iterate over the errors that have been added to the collection."""
        return self._errors.iterkeys()

    def has_target(self, target):
        """Return whether an error has been assigned to the given iterable of
        field names."""
        targetset = set(target)
        return any(i == targetset for i in self.targets())

    def has_partial_target(self, target):
        """Return whether the given iterable of field names forms an improper
        subset of any target."""
        targetset = set(target)
        return any(targetset.issubset(i) for i in self.targets())

    def errors_for(self, target):
        """Return a set of errors that have been assigned to ``target``, an
        iterable of field names whose order is insignificant.  Raise
        ``KeyError`` if no errors have been assigned."""
        if not self.has_target(target):
            raise KeyError('%r is not a target' % target)
        targetset = set(target)
        return set(msg for msg, targets in self._errors.iteritems()
                   if targetset in targets)

    def targets_for(self, error):
        """Return a set of targets to which ``error`` has been assigned.
        Raise ``KeyError`` if no such error has been added."""
        return self._errors[error]

    def sorted_targets(self, field_order):
        """Return the error set's targets sorted primarily by the number of
        fields in the target, and then by the position of the first field in
        each target list as indicated by ``field_order``."""
        def cmp_fields(x, y):
            xi = yi = -1
            for i, value in enumerate(field_order):
                if value == x:
                    xi = i
                elif value == y:
                    yi = i
            return cmp(xi, yi)

        targets_by_size = {}
        for target in self.iter_targets():
            sorted_fields = tuple(sorted(target, cmp_fields))
            try:
                targets_by_size[len(target)].add(sorted_fields)
            except KeyError:
                targets_by_size[len(target)] = set()
                targets_by_size[len(target)].add(sorted_fields)

        for i in targets_by_size:
            targets_by_size[i] = list(targets_by_size[i])
            targets_by_size[i].sort(lambda x, y: cmp_fields(x[0], y[0]))

        targetlist = []
        for i in sorted(targets_by_size):
            targetlist += targets_by_size[i]

        return targetlist

    def __str__(self):
        return self._pretty_print(self)

    def _pretty_print(self, errorset, depth=0, step=2):
        messages = []
        indent = (' ' * depth * step)
        for target in errorset.targets():
            targetlabel = ', '.join(sorted(target))
            for error in errorset.errors_for(target):
                if isinstance(error, basestring):
                    messages.append('%s* %s [%s]' % (indent, error, targetlabel))
                elif isinstance(error, ErrorSet):
                    messages.append('%s* [%s]' % (indent, targetlabel))
                    messages.append(self._pretty_print(error, depth + 1))
        return '\n'.join(messages)


class Form(object):
    """Translator of user input into Python values.

    Forms translate a ``MultiDict`` of user input to a ``dict`` of validated
    Python values suitable for safe use by an application.  A form is composed
    by any number of fields, and each field is assigned a key.  This key is
    where the field's translated value is stored in the resulting ``dict``.

    Forms may also have any number of validators to enforce higher-order
    constraints across multiple fields.  Validators execute after the form's
    fields have been translated.  Validators execute even if one or more
    fields raise ``TranslationError``, though any offending field is omitted
    from their input.  (Because of this, most validators skip validation if
    one or more expected value is missing.)"""

    def __init__(self, fields, validators=[]):
        """Create a new form with the ``fields``, a mapping of keys to fields,
        and optional iterable of ``validators``."""
        self.fields = fields
        self.validators = validators

    def translate(self, input):
        """Return a ``dict`` of valid Python values translated from a
        ``MultiDict`` of user input.  Raise ``ErrorSet`` if one or more field
        raises ``TranslationError``, or if any validator raises
        ``ValidationError``."""
        values = {}
        errors = ErrorSet()

        for key, field in self.fields.iteritems():
            try:
                values[key] = field.translate(input)
            except TranslationError, e:
                errors.add(e.message, set([key]))
            except ErrorSet, e:
                errors.add(e, set([key]))

        for validator in self.validators:
            try:
                validator.validate(values)
            except ValidationError, e:
                errors.add(e.message, e.keys)

        if errors:
            raise errors
        else:
            return values
